ControlValueAccessor
interface.The ControlValueAccessor Interface
ControlValueAccessor
is an interface for communication between a FormControl
and the native element. It abstracts the operations of writing a value and listening for changes in the DOM element representing an input control. The following snippet was taken from the Angular source code, along with the original comments:
The ControlValueAccessor interface.
interface ControlValueAccessor { /** * Write a new value to the element. */ writeValue(obj: any): void; /** * Set the function to be called when the control receives a change event. */ registerOnChange(fn: any): void; /** * Set the function to be called when the control receives a touch event. */ registerOnTouched(fn: any): void; /** * This function is called when the control status changes to or from "DISABLED". * Depending on the value, it will enable or disable the appropriate DOM element. * @param isDisabled */ setDisabledState?(isDisabled: boolean): void; }
ControlValueAccessor Directives
Each time you use the formControl
or formControlName
directive on a native <input>
element, one of the following directives is instantiated, depending on the type of the input:
- DefaultValueAccessor – Deals with all input types, excluding checkboxes, radio buttons, and select elements.
- CheckboxControlValueAccessor – Deals with checkbox input elements.
- RadioControlValueAccessor – Deals with radio control elements [RH: Or just “radio buttons”
or “radio button inputs”?]. - SelectControlValueAccessor – Deals with a single select element.
- SelectMultipleControlValueAccessor – Deals with multiple select elements.
Become part of our International JavaScript Community now!
LEARN MORE ABOUT iJS:
Let’s peek under the hood of the CheckboxControlValueAccessor
directive to see how it implements the ControlValueAccessor
interface. The following snippet was taken from the Angular docs:
checkbox_value_accessor.ts.
import {Directive, ElementRef, Renderer, forwardRef} from '@angular/core'; import {ControlValueAccessor, NG_VALUE_ACCESSOR} from './control_value_accessor'; export const CHECKBOX_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => CheckboxControlValueAccessor), multi: true, }; @Directive({ selector : `input[type=checkbox][formControlName], input[type=checkbox][formControl], input[type=checkbox][ngModel]`, host : { '(change)': 'onChange($event.target.checked)', '(blur)' : 'onTouched()' } , providers: [CHECKBOX_VALUE_ACCESSOR] }) export class CheckboxControlValueAccessor implements ControlValueAccessor { onChange = (_: any) => {}; onTouched = () => {}; constructor(private _renderer: Renderer, private _elementRef: ElementRef) {} writeValue(value: any): void { this._renderer.setElementProperty(this._elementRef.nativeElement, 'checked', value); } registerOnChange(fn: (_: any) => {}): void { this.onChange = fn; } registerOnTouched(fn: () => {}): void { this.onTouched = fn; } setDisabledState(isDisabled: boolean): void { this._renderer.setElementProperty(this._elementRef.nativeElement, 'disabled', isDisabled); } }
Let’s explain what’s going on:
- This directive is instantiated when an input of type
checkbox
is declared with theformControl
,formControlName
, orngModel
directives. - The directive listens to
change
andblur
events in thehost
. - This directive will change both the
checked
anddisabled
properties of the element, so the
ElementRef
andRenderer
[RH: ElementRef and Renderer what? Classes?] are injected. - The
writeValue()
implementation is straight forward: it sets thechecked
property of the
native element. Similarly,setDisabledState()
sets thedisabled
property. - The function being passed to the
registerOnChange()
method is responsible for updating the outside world about changes to the value. It is called in response to achange
event with the input value. - The function being passed to the
registerOnTouched()
method is triggered by theblur
event. - Finally, the
CheckboxControlValueAccessor
directive is registered as a provider.
Sample Custom Form Control: Button Group
Let’s build a custom FormControl
based on the Twitter Bootstrap button group component.
We will start with a simple component:
custom-control.component.ts.
import {Component} from "@angular/core"; @Component({ selector : 'rf-custom-control', templateUrl: 'custom-control.component.html', }) export class CustomControlComponent { private level: string; private disabled: boolean; constructor(){ this.disabled = false; } public isActive(value: string): boolean { return value === this.level; } public setLevel(value: string): void { this.level = value; } }
Here is the template:
custom-control.component.html.
<div class="btn-group btn-group-lg"> <button type="button" class="btn btn-secondary" [class.active]="isActive('low')" [disabled]="disabled" (click)="setLevel('low')">low</button> <button type="button" class="btn btn-secondary" [class.active]="isActive('medium')" [disabled]="disabled" (click)="setLevel('medium')">medium</button> <button type="button" class="btn btn-secondary" [class.active]="isActive('high')" [disabled]="disabled" (click)="setLevel('high')">high</button> </div>
Next, let’s implement the ControlValueAccessor
interface:
custom-control.ts component class.
export class CustomControlComponent implements ControlValueAccessor { private level: string; private disabled: boolean; private onChange: Function; private onTouched: Function; constructor() { this.onChange = (_: any) => {}; this.onTouched = () => {}; this.disabled = false; } public isActive(value: string): boolean { return value === this.level; } public setLevel(value: string): void { this.level = value; this.onChange(this.level); this.onTouched(); } writeValue(obj: any): void { this.level = obj; } registerOnChange(fn: any): void{ this.onChange = fn; } registerOnTouched(fn: any): void { this.onTouched = fn; } setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; } }
The last step is to register our custom control component under the NG_VALUE_ACCESSOR
token. NG_VALUE_ACCESSOR
is an OpaqueToken
used to register multiple ControlValue
providers. (If you are not familiar with OpaqueToken
, the multi
property, and the forwardRef()
function, read the official dependency injection guide on the Angular website.)
Here’s how we register the CustomControlComponent
as a provider:
Registering the control as a provider.
const CUSTOM_VALUE_ACCESSOR: any = { provide : NG_VALUE_ACCESSOR, useExisting: forwardRef(() => CustomControlComponent), multi : true, }; @Component({ selector : 'app-custom-control', providers : [CUSTOM_VALUE_ACCESSOR], templateUrl: 'custom-control.component.html', })
Our custom control is ready. Let’s try it out:
app.component.ts.
import {Component, OnInit} from "@angular/core"; import {FormControl} from "@angular/forms"; @Component({ selector: 'rf-root', template: ` <div class="container"> <h1 class="h1">REACTIVE FORMS</h1> <rf-custom-control [formControl]="buttonGroup"></rf-custom-control> <pre> <code> Control dirty: {{buttonGroup.dirty}} Control touched: {{buttonGroup.touched}} </code> </pre> </div> `, }) export class AppComponent implements OnInit { public buttonGroup: FormControl; constructor() { this.buttonGroup = new FormControl('medium'); } ngOnInit(): void { this.buttonGroup.valueChanges.subscribe(value => console.log(value)); } }
This tutorial is an excerpt from iJS speaker Nir Kaufman’s eBook “Angular Reactive Forms – A comprehensive guide for building forms with Angular”. The complete book can be purchased in the Leanpub store: https://leanpub.com/angular-forms